iT邦幫忙

2024 iThome 鐵人賽

DAY 10
0

在講完了作用域之後,我們終於可以來講關於 this。

this 可能是許多人一直困惑的點,他究竟是什麼?
我們可以這樣說:他是一個總是存在於所有作用域中的特殊關鍵字,總是指向一個對象。

那問題來了,他為什麼要存在,他又到底指向誰?

這兩個問題其實是緊密相連的。
怎麼說?this實際上會隨著被 調用 的時機(Call-site)指向不同的對象,即使是同一個語句中的 this,都有可能因為被使用的方式不同,得到不同的結果。
指向對象的則是透過一連串的規則來決定。

很明顯,這種靈活的指向解決的問題就是高複用性彈性

function hello(){
	return `${this.name} says hello!`;
}
let friend1 = { name: 'Ken'};
let friend2 = { name: 'Ryu'};
console.log(hello.call(friend1)); // Ken says hello!
console.log(hello.call(friend2)); // Ryu says hello!

在上面的例子我們可以看到,thishello 中指向了 friend1friend2
即使在 hello 函世中我們本身並沒有定義 name 的屬性,透過了 this 關鍵字,增加了這個函式的靈活性。

this 的指向規則

以下的介紹順序具有順序性,最先介紹的優先度最低。
這邊關於綁定的名稱來自 YDKJS

  1. 默認綁定(Default Binding)
  2. 隱含綁定(Implicit Binding)
  3. 明確綁定(Explicit Binding)
  4. new 綁定(new Binding)

規格外 : ES 6 箭頭函式

1. 默認綁定(Default Binding)

在所有情況都不符合的情況下,就會套用默認綁定,獨立調用函式也會是這種方式。
在嚴格模式下,默認綁定 this 會變成 undefined
非嚴格模式下,this 則會指向全域。

function foo(){
    console.log(this.a);
}
a = 10
foo();//
function bar(){
    "use strict"
    console.log(this.b);
}
b = 10;
bar();//Uncaught TypeError: Cannot read properties of undefined (reading 'b')

2. 隱含綁定(Implicit Binding)

調用點具備相對應的對象的時候。

function foo(){
    console.log(this.a);
}
let obj = {
    a:10,
    foo:foo
};
obj.foo();//10

上面的例子 .foo() 作為 obj 的屬性被呼叫。
實際調用點是 .foo() 而不是 foo:foofoo:foo 僅僅只指向位址,並未調用。
一般這種適用於方法作為物件屬性被呼叫時套用。

function foo(){
    console.log(this.a);
}
let obj2 = {
    a:20,
    foo:foo
}
let obj = {
    a:10,
    obj2:obj2
}
obj.obj2.foo();//20

這個情況的調用點發生在 obj2.foo() 上,所以回傳的會是 obj2 上的 20。

function foo() {
	console.log( this.a );
}
function doFoo(fn) {
	fn(); 
}
let obj = {
	a: 'obj a',
	foo: foo
};
let a = "global a";
doFoo( obj.foo );//global a 

上面這個 obj.foo 看起來很像我們說的物件上的方法,但如 foo:foo 一樣,他只是一個位址,並不是方法。
上面的例子的調用發生在 doFoofn() 這個語句,因為 doFoo() 是以全域方法的方式被呼叫,所以是套用 1.默認綁定 規則,往外查找到了全域的 a

這段想再次強調什麼是調用點,什麼不是,this只關注物件被調用的地方來決定指向對象。

3. 明確綁定(Explicit Binding)

前面兩種綁定的行為看起來都是有點背後規則的感覺,一如他們的名字 默認,隱含,在明確綁定中,我們要討論的是如何指名 this 的指向對象。

function foo(){
    console.log(this.a);
}
let obj = {
    a : 'obj a'
};
let a = 'global a';
foo.call(obj);//obj a
foo.apply(obj);//obj a 

foo.call() 的方法就是第一種明確綁定的方式,方法簽名是 Function.prototype.call(thisArg)(也能接收更多的參數作為函式的參數)。透過這個方法,被執行的函式的 this 會指第一個被傳入的參數 thisArg
以上面的例子,傳入 obj,因此印出了 obj 裡面的 a 而不是全域的 a

Function.prototype.apply(thisArg) 的使用情境上和 Function.prototype.call(thisArg) 主要在帶入函式的參數方式,apply 以陣列方式帶入,call 以明確參數序依序帶入 -- 但在 this 的綁定上,他們皆屬明確綁定,都會將 this 綁定到第一個傳入的參數上。

硬綁定(Hard Binding)與 bind 關鍵字

明確綁定讓綁定的意圖更明顯,能夠簡單的找到 this 指向的對象,但缺點是仍有可能遇到 this 被修改或遺失的狀況。
這時有種設計模式可以解決這個問題,讓 this 不會被改動,稱作 硬綁定(Hard Binding)。

function foo(word) {
	console.log( this.a, word );
	return this.a + " " + word;
}
let obj = {
	a: 'obj a'
};

let bar = function() {
	return foo.apply( obj, arguments );
};
let a = 'global a'
let b = bar('parm'); //"obj a", "param"
console.log(b); //"obj a param"

bar(3) 是調用點,此時 bar 內部的 this 套用默認綁定規則,應該會吃到 a = 'global a'
但進入 bar 後的 foo 透過 apply 明確綁定了 this 為全域的 obj 物件。
接著執行了 foo('param')this.a 指向的是 obj.a,所以印出了 "obj a", "param"

因為 bar 的內部透過 apply 做了明確綁定,因此外部的 b(由 bar 建立的物件),不管如何執行,都保證 foo 所使用的的 this 必定為 obj,且無法從外部更動這個綁定行為。(即使外部對 bar 使用 bar.call 也不影響,因為實際上使用 this 的是 foo,無法從外部被改動)。

因為此種設計模式的常用,出現了與之對應的語法關鍵字 bind,這個關鍵字自 ES 5 就存在。

function foo(word) {
	console.log( this.a, word );
	return this.a + " " + word;
}
let obj = {
	a: 'obj a'
};

let bar = foo.bind(obj);
let a = 'global a'
let b = bar('parm'); //"obj a", "param"
console.log(b); //"obj a param"

這樣的寫法效果與硬綁定第一種一樣,也是硬綁定,因為綁定行為建立在 bar 裡,確保 foothis 能總是指向希望的對象且不被更改。

4. new 綁定(new Binding)

new 綁定指的是使用 new 關鍵字時對生成物件this 綁定行為。
當透過 new 建構出了一個新的物件,該物件內部的所有 this 都會指向生成的物件本身。

function foo(a) {
	this.a = a;
}

let a = 'global a';
let bar = new foo('input a');
console.log(bar);//{a: "input a",}
  

bar 是透過 new 建立的 foo 物件,因此 foo 物件中方法定義的 this 都會指向 bar 這個物件。
如同建構時傳入的 'input a',掛到了 bar 本身的屬性上。(this.a = a 語句的結果,這邊的 this 指的就是 bar)。

綁定順序實驗

雖然說了優先順序上是 new 綁定 > 明確綁定 > 隱含綁定 > 默認綁定,但身為工程師,總是要有實證的精神。
默認綁定是最不優先的,在上面的例子應該看得很清楚了,所以我們先驗證明確綁定和隱含綁定的優先序。

function foo(){
    console.log(this.a);
}
let obj1 = {
    a : 'obj1 a',
    foo : foo
};
let obj2 = {
    a : 'obj2 a',
    foo : foo
};
let a = 'global a';
obj1.foo();//'obj1 a' 隱含綁定
obj2.foo();//'obj2 a' 隱含綁定
obj1.foo.call(obj2);//'obj2 a' 明確綁定
obj2.foo.call(obj1);//'obj1 a' 明確綁定

前兩行 .foo() 確認了隱含綁定的生效,表示調用物件上的方法時隱含綁定如預期的發生。
但是如果我們透過 .call() 方法,來指定物件上方法的 thisArg,可以看到後兩行 this 都指向了指定的 thisArg,由此可知,明確綁定是優先於隱含綁定的。

接著,我們該來驗證的是 new 綁定規則是否優先於明確綁定。
因為語法上的限制,newcall 無法一起使用,所以我們要使用應綁定的關鍵字 bind 來證明這件事。

function foo(input) {
	this.a = input;
}
let obj1 = {};
let bar = foo.bind(obj1);
bar('input bar');
console.log( obj1.a );//"input bar"

let baz = new bar('input baz');
console.log( obj1.a );//"input bar" 
console.log( baz.a );//"input baz"

透過 bar('input bar') foo.bind(obj1),我們指定了 foothis 指向 obj1,並把他的 a 放為 'input bar',這是硬綁定,外部無法直接更改 foo 綁定 obj1 的行為。

這時候我們使用 new 關鍵字來從 bar 上建構新的物件baz,這邊 new綁定的規則是把 this 指向物件 baz,覆寫物件的a'input baz',結果 new 綁定的複寫生效, baz.a印出的結果是 new 時傳進去的字串。而 obj1.a 上的物件若硬綁定生效的話,應該預期被改為 input baz,但仍維持著 input bar
由此可見即使是硬綁定,透過 new 關鍵字仍能覆寫其 this 綁定。

判定 this 順序

綜上所述,確認一個物件的 this 關鍵字指向誰的判定順序,由上至下找到第一個符合的結果

  1. 如果由 new 創建,則該物件的 this 指向建構出的物件
  2. 如果執行了 callapplybind,則該物件的 this 指向傳入的 thisArg
  3. 如果調用物件上的方法,使用的 this 視調用點而定,若直接調用 obj.func()this 指向 obj 本身
  4. 如果以上皆不符合,則嚴格模式this 指向undefined,非嚴格模式 this 指向全域

以上概括了大部分的 this 規則 -- 除了一個特例, ES 6 引入的箭頭函式,這個我們放在明天討論 function 的時候來討論。
希望以上內容已經足以讓你對 this 是一個怎麼樣的屬性,如何指向有了足夠的了解。


上一篇
作用域(Scope),let,var 與 const
下一篇
函式(function)
系列文
Don't make JavaScript Just Surpise31
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言